作为业界最知名的Java框架,Spring几乎是Java developer必须修炼的外功,全面地了解Spring是很有必要的。
What
首先,需要理清楚Spring的各种概念,避免混淆。
Spring Framework
Spring Framework是一个大引擎,也是Spring项目的核心部分,主要由20+个组件/模块构成,分为几大类:
- 核心容器
核心部分,提供最重要的IoC/DI功能,包含Core、Beans、Context等组件。 - 数据处理与集成
提供与数据库交互的抽象,如对JDBC、ORM等的封装。 - Web
提供基于Servlet的Web服务支撑。 - AOP
提供面向切面编程的支持。 - 测试
提供与JUnit等测试组件对接的支持。
Spring MVC
Spring MVC是Spring Framework的Web类模块中的一个,是一个提供Web服务的基础框架。
Spring Boot
Spring Boot是一个基于Spring Framework的解决方案套件或一个快捷方式。它集成了Web服务器、实现了Spring Framework的自动化配置以及解决了许多常用包的依赖问题,帮developer做了很多准备工作,因此developer可以很快地上手写业务代码而不必过于关注Spring的部分。
Spring Cloud
Spring Cloud基于Spring Boot,目的是解决更上层的问题,提供了一套完善的微服务框架,例如:
- 分布式配置
多个Spring Boot服务使用分布式配置服务进行配置。 - 服务注册/发现/路由/调用
多个Spring Boot服务间能够很方便地互相发现和调用。 - 客户端负载均衡
由Spring Boot服务进行负载均衡计算来决策调用其他服务的哪个实例。
这其中涉及到上层的内容在此不作进一步展开,详细的内容可参考《微服务二三事》。
IoC
理清了各种五花八门的名词/框架/概念,让我们回归核心:IoC。
IoC(Inversion of Control)控制反转,光是这个词就能直接带出3个问题:
- 什么的控制?
- 为何反转?
- 如何反转?
让我们依次解答这几个问题。
在传统的Java代码里,我们面对那些Has-A的结构时是怎么做的呢?1
2
3
4
5
6
7
8
9public class B {
private A a;
public B(A a) {
this.a = a;
}
}
// somewhere else in code
A a = new A();
B b = new B(a);
这样的耦合度较强,代码中至少有一处需要:
- 显示或隐式地创建A对象。
- 传入B类的构造方法。
对于每一处Has-A都需要程序代码进行上述控制,当复杂度升高时难以掌控。
而IoC代替程序代码包办了这层控制,从而降低耦合度,程序代码只需向IoC控制器“要”依赖的对象,而不是自己“造”。
因此回答了头两个问题:反转的“控制”是对象间的依赖,使用一个统一的对象依赖控制器,目的是为了解耦。
可见,IoC具有以下特点:
- 体现软件工程的原则之一:解耦。
- 对实例的控制由程序转移到了控制器/容器/框架。
- 为模块化提供了支撑。
实现
IoC只是一种设计,其实现并非Spring不可,例如可用策略模式+工厂模式来实现,当然也可以用依赖注入。
DI
DI(Dependency Injection)依赖注入是一种IoC的具体实现,被反转的控制是对象依赖的注值。以往程序代码主动为对象的依赖赋值,如今由DI的控制器统一处理。
Spring IoC & DI
Spring IoC定义了以下概念:
- Bean
等待被注入的依赖对象。 - 容器
上文提到的控制反转的控制器,用于管理所有Bean的依赖链。
在实际应用中,Spring DI提供三种方式注值:
- 构造方法注入。
- Setter方法注入。
- 成员变量注入。
Spring根据配置文件或注解进行Bean依赖的管理,主要完成了3项任务:
- 根据配置文件或注解,解析出Bean之间完整的依赖图。
- 利用Java反射,用适当的方式创建出Bean,保存到一个容器内。
- 再次利用反射,在适当的时机将被依赖的Bean作为成员变量注入到依赖其的Bean中。
AOP
AOP(Aspect-Oriented Programming)面向切面编程,是一种对OOP纵向结构丰富而横向结构缺失的补充。Java OOP可以提供复杂而纵深的继承体系,但是非继承关系的横向加强功能并不是那么容易实现。
例如,当很多类的方法都调用某一个类的成员方法时,需要这些类都持有该类的对象,且这些类的所有对象都需要显示地依赖该对象。
1 | public class A { |
可以看到,A和B两个类中的部分代码几乎完全重复,而且与其业务不相关联,能否用某种方式实现同样的功能,而不需要developer写重复的代码逻辑呢?
一种方式是利用编译器将这些功能代码在编译阶段“织入”源代码,但这需要特殊的编译器。
另一种方式是利用动态代理在运行时将这些功能代码切入。
动态代理
Java的动态代理主要有JDK代理和CGLIB代理两种。
JDK代理
被代理的目标类需实现一个接口(业务相关的接口),没有在该接口定义的方法不能被代理,Spring AOP默认采用此方式进行代理,只有当被代理的类没有实现接口时才会切换到CGLIB代理。
CGLIB代理
这种方式需要CGLIB支持,相当于创建一个被代理的类的非继承代理子类(组合),因此也被称为子类代理。
概念
AOP引入了一系列概念,均和将功能代码切入有关。
通知Advice
定义了When和What,即切面的切入时机以及进行哪些行为。
可以想象,针对一个方法,合理的切入时机包括:
- 方法调用前。
- 方法执行后,不考虑返回。
- 方法返回后。
- 方法抛出异常后。
- 方法前以及方法后。
连接点JoinPoint
连接点可以理解为通知定义的上述切入时机。
切点PointCut
定义了Where,即匹配那些需要切入的方法,允许具体的方法名、类名、正则匹配等。
切面Aspect
在通知与切点的配合下,切面就成型了,程序就知道切入的When、Where、What。
引入Introducion
上述切面只是针对指定方法的增强,而引入可以针对类进行增强,为其动态地添加新的方法。
应用
最常见的两类应用就是权限控制和日志,此外还有缓存、调试、性能检测,甚至一定程度的事务处理。